Провести оценку результатов A/B-теста. Оцените корректность проведения теста и проанализируйте его результаты.
Название теста: recommender_system_test;
Группы: А (контрольная), B (новая платёжная воронка);
Дата запуска: 2020-12-07;
Дата остановки набора новых пользователей: 2020-12-21;
Дата остановки: 2021-01-04;
Аудитория: 15% новых пользователей из региона EU;
Назначение теста: тестирование изменений, связанных с внедрением улучшенной рекомендательной системы;
Ожидаемое количество участников теста: 6000.
Ожидаемый эффект: за 14 дней с момента регистрации в системе пользователи покажут улучшение каждой метрики не менее, чем на 10%:
конверсии в просмотр карточек товаров — событие product_page
просмотры корзины — product_cart
покупки — purchase.
Загрузите данные теста, проверьте корректность его проведения и проанализируйте полученные результаты.
ab_project_marketing_events.csv
final_ab_new_users.csv
final_ab_events.csv
final_ab_participants.csv
/datasets/ab_project_marketing_events.csv — календарь маркетинговых событий на 2020 год;
Структура файла:
name — название маркетингового события;
regions — регионы, в которых будет проводиться рекламная кампания;
start_dt — дата начала кампании;
finish_dt — дата завершения кампании.
/datasets/final_ab_new_users.csv — все пользователи, зарегистрировавшиеся в интернет-магазине в период с 7 по 21 декабря 2020 года;
Структура файла:
user_id — идентификатор пользователя;
first_date — дата регистрации;
region — регион пользователя;
device — устройство, с которого происходила регистрация.
/datasets/final_ab_events.csv — все события новых пользователей в период с 7 декабря 2020 по 4 января 2021 года;
Структура файла:
user_id — идентификатор пользователя;
event_dt — дата и время события;
event_name — тип события;
details — дополнительные данные о событии. Например, для покупок, purchase, в этом поле хранится стоимость покупки в долларах.
/datasets/final_ab_participants.csv — таблица участников тестов.
Структура файла:
user_id — идентификатор пользователя;
ab_test — название теста;
group — группа пользователя.
Проверить целостность данных и правильность формирования групп в соотвествии с техническим заданием. Проверить пересечение с другими маркетинговыми активностями компании и другими тестами исходных данных тестирования. Исследовать равномерность распределения пользователей по тестовым группам и правильность их формирования.
1. Загрузка и предобработка
1.1. Открытие и изучение файлов с данными
2. Подготовка данных и проверка условий ТЗ
2.1. Сводная таблица
2.2. Проверка ТЗ - количество пользователей > 6000 и 15% от зарегистрированных в Европе
2.3. Добавляем в сводную таблицу условие ТЗ - 14 дней после регистрации
2.4. Проверка старта, окончания и остановки теста условиям ТЗ
2.6 Пересечение групп, маркетинговых активностей и тестов
2.6.1 Анализ совпадения теста и маркетинговых событий
2.6.2 Анализ на пересечение с тестом interface_eu_test
2.7 Итоговые таблицы фильтрация в соответствии с ТЗ
3. Исследовательский анализ
3.1 Исследовательский анализ количество пользователей по группам А и В
3.2. Исследовательский анализ количество пользователей по девайсам
3.3. Исследовательский анализ, количество событий совершаемых пользователями
3.4. Анализ распределения количества событий на пользователям
3.5. Динамика распределения событий по дням, Анализ
3.6. Воронки событий
3.6.1 Простая продуктовая воронка
3.6.2 Продуктовая воронка с учётом последовательности событий
4. Статистический анализ
4.1 Проверка статистической разницы долей z-критерием
5. Выводы и рекомендации
# импортируем все необходимые библиотеки
import pandas as pd
import datetime as dt
from datetime import datetime, timedelta
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import plotly.express as px
from plotly import graph_objects as go
from pandas.plotting import register_matplotlib_converters
import scipy.stats as st
import math as mth
import warnings
import warnings
warnings.filterwarnings("ignore")
#загружаем даннык из файла ab_project_marketing_events
ab_project_marketing_events = pd.read_csv('/datasets/ab_project_marketing_events.csv')
display(ab_project_marketing_events.head(20)) #выводим таблицу
ab_project_marketing_events.info()
| name | regions | start_dt | finish_dt | |
|---|---|---|---|---|
| 0 | Christmas&New Year Promo | EU, N.America | 2020-12-25 | 2021-01-03 |
| 1 | St. Valentine's Day Giveaway | EU, CIS, APAC, N.America | 2020-02-14 | 2020-02-16 |
| 2 | St. Patric's Day Promo | EU, N.America | 2020-03-17 | 2020-03-19 |
| 3 | Easter Promo | EU, CIS, APAC, N.America | 2020-04-12 | 2020-04-19 |
| 4 | 4th of July Promo | N.America | 2020-07-04 | 2020-07-11 |
| 5 | Black Friday Ads Campaign | EU, CIS, APAC, N.America | 2020-11-26 | 2020-12-01 |
| 6 | Chinese New Year Promo | APAC | 2020-01-25 | 2020-02-07 |
| 7 | Labor day (May 1st) Ads Campaign | EU, CIS, APAC | 2020-05-01 | 2020-05-03 |
| 8 | International Women's Day Promo | EU, CIS, APAC | 2020-03-08 | 2020-03-10 |
| 9 | Victory Day CIS (May 9th) Event | CIS | 2020-05-09 | 2020-05-11 |
| 10 | CIS New Year Gift Lottery | CIS | 2020-12-30 | 2021-01-07 |
| 11 | Dragon Boat Festival Giveaway | APAC | 2020-06-25 | 2020-07-01 |
| 12 | Single's Day Gift Promo | APAC | 2020-11-11 | 2020-11-12 |
| 13 | Chinese Moon Festival | APAC | 2020-10-01 | 2020-10-07 |
<class 'pandas.core.frame.DataFrame'> RangeIndex: 14 entries, 0 to 13 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 name 14 non-null object 1 regions 14 non-null object 2 start_dt 14 non-null object 3 finish_dt 14 non-null object dtypes: object(4) memory usage: 576.0+ bytes
Таблица маркетинговых активностей содержит данные о 10-ти маркетинговых мероприятиях с 25 января 2020 года до 3 января 2021 года.
Только одно маркетинговое мероприятие попадает в исследуемый нами период с 7 по 21 декабря 2020 года - это "Christmas&New Year Promo", оно начинается после набора новых пользователей в группы тестирования, но действует в период проведения теста после набора. Неизвестно как это маркетинговое событие повлияет на метрики исследуемых групп, тем более что однозначной привязки пользователя к событию сделать нельзя (ключ к соединению только страна проведения этого события). Принимал наш воображаемый пользователь участие в рождественском промо или нет - неизвестно, возможно мы увидим это в дальнейшем на графиках.
#загружаем даннык из файла final_ab_new_users
final_ab_new_users = pd.read_csv('/datasets/final_ab_new_users.csv')
final_ab_new_users['first_dt'] = pd.to_datetime(final_ab_new_users['first_date'])
final_ab_new_users['first_date'] = pd.to_datetime(final_ab_new_users['first_date']).dt.date
display(final_ab_new_users.head())
display(final_ab_new_users.info())
display(final_ab_new_users['first_dt'].describe())
display('количество дубликатов', final_ab_new_users.duplicated().sum())
| user_id | first_date | region | device | first_dt | |
|---|---|---|---|---|---|
| 0 | D72A72121175D8BE | 2020-12-07 | EU | PC | 2020-12-07 |
| 1 | F1C668619DFE6E65 | 2020-12-07 | N.America | Android | 2020-12-07 |
| 2 | 2E1BF1D4C37EA01F | 2020-12-07 | EU | PC | 2020-12-07 |
| 3 | 50734A22C0C63768 | 2020-12-07 | EU | iPhone | 2020-12-07 |
| 4 | E1BDDCE0DAFA2679 | 2020-12-07 | N.America | iPhone | 2020-12-07 |
<class 'pandas.core.frame.DataFrame'> RangeIndex: 61733 entries, 0 to 61732 Data columns (total 5 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 61733 non-null object 1 first_date 61733 non-null object 2 region 61733 non-null object 3 device 61733 non-null object 4 first_dt 61733 non-null datetime64[ns] dtypes: datetime64[ns](1), object(4) memory usage: 2.4+ MB
None
count 61733 unique 17 top 2020-12-21 00:00:00 freq 6290 first 2020-12-07 00:00:00 last 2020-12-23 00:00:00 Name: first_dt, dtype: object
'количество дубликатов'
0
В таблице final_ab_new_users — все пользователи, зарегистрировавшиеся в интернет-магазине в период с 7 по 21 декабря 2020 года. Даты старта регистрации и данных таблицы совпадают, дата окончания набора новых пользователей в группы по А/В тестированию на 2 дня раньше чем данные этой таблицы. Значит в ней есть лишние пользователи.
Пропусков и дубликатов нет.
#загружаем даннык из файла final_ab_events
final_ab_events = pd.read_csv('/datasets/final_ab_events.csv')
display(len(final_ab_events['event_dt'].unique()))
final_ab_events['event_dt'] = pd.to_datetime(final_ab_events['event_dt'])
final_ab_events['event_date'] = pd.to_datetime(final_ab_events['event_dt']).dt.date
display(final_ab_events.head(10)) #выводим таблицу
display(final_ab_events.info())
display('количество дубликатов', final_ab_events.duplicated().sum())
final_ab_events = final_ab_events.fillna(0)
267268
| user_id | event_dt | event_name | details | event_date | |
|---|---|---|---|---|---|
| 0 | E1BDDCE0DAFA2679 | 2020-12-07 20:22:03 | purchase | 99.99 | 2020-12-07 |
| 1 | 7B6452F081F49504 | 2020-12-07 09:22:53 | purchase | 9.99 | 2020-12-07 |
| 2 | 9CD9F34546DF254C | 2020-12-07 12:59:29 | purchase | 4.99 | 2020-12-07 |
| 3 | 96F27A054B191457 | 2020-12-07 04:02:40 | purchase | 4.99 | 2020-12-07 |
| 4 | 1FD7660FDF94CA1F | 2020-12-07 10:15:09 | purchase | 4.99 | 2020-12-07 |
| 5 | 831887FE7F2D6CBA | 2020-12-07 06:50:29 | purchase | 4.99 | 2020-12-07 |
| 6 | 6B2F726BFD5F8220 | 2020-12-07 11:27:42 | purchase | 4.99 | 2020-12-07 |
| 7 | BEB37715AACF53B0 | 2020-12-07 04:26:15 | purchase | 4.99 | 2020-12-07 |
| 8 | B5FA27F582227197 | 2020-12-07 01:46:37 | purchase | 4.99 | 2020-12-07 |
| 9 | A92195E3CFB83DBD | 2020-12-07 00:32:07 | purchase | 4.99 | 2020-12-07 |
<class 'pandas.core.frame.DataFrame'> RangeIndex: 440317 entries, 0 to 440316 Data columns (total 5 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 440317 non-null object 1 event_dt 440317 non-null datetime64[ns] 2 event_name 440317 non-null object 3 details 62740 non-null float64 4 event_date 440317 non-null object dtypes: datetime64[ns](1), float64(1), object(3) memory usage: 16.8+ MB
None
'количество дубликатов'
0
В таблице final_ab_events — все события новых пользователей в период с 7 декабря 2020 по 4 января 2021 года. Всего событий 440317, пропуски есть только в столбце details - но для нашего исследования это не критично. Для удобства дальнейшего использования данных заменияем пропуски в столбце details на 0, потому что деталей нет, а данные нам понадобятся для исследования.
Дубликатов нет.
Дата последнего события 30 декабря 2020 года.
#загружаем даннык из файла final_ab_participants
final_ab_participants = pd.read_csv('/datasets/final_ab_participants.csv')
display(final_ab_participants.head(10)) #выводим таблицу
display(final_ab_participants.info())
display('количество дубликатов', final_ab_participants.duplicated().sum())
| user_id | group | ab_test | |
|---|---|---|---|
| 0 | D1ABA3E2887B6A73 | A | recommender_system_test |
| 1 | A7A3664BD6242119 | A | recommender_system_test |
| 2 | DABC14FDDFADD29E | A | recommender_system_test |
| 3 | 04988C5DF189632E | A | recommender_system_test |
| 4 | 482F14783456D21B | B | recommender_system_test |
| 5 | 4FF2998A348C484F | A | recommender_system_test |
| 6 | 7473E0943673C09E | A | recommender_system_test |
| 7 | C46FE336D240A054 | A | recommender_system_test |
| 8 | 92CB588012C10D3D | A | recommender_system_test |
| 9 | 057AB296296C7FC0 | B | recommender_system_test |
<class 'pandas.core.frame.DataFrame'> RangeIndex: 18268 entries, 0 to 18267 Data columns (total 3 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 18268 non-null object 1 group 18268 non-null object 2 ab_test 18268 non-null object dtypes: object(3) memory usage: 428.3+ KB
None
'количество дубликатов'
0
final_ab_participants['ab_test'].unique()
array(['recommender_system_test', 'interface_eu_test'], dtype=object)
final_ab_participants — таблица участников тестов содержит данные об участниках 2-х видов тестов: 'recommender_system_test'и 'interface_eu_test'. Всего 18268 записей., пропусков и дубликатов нет.
В таблицах содержатся полные данные всех полей которые на могут понадобиться, дубликатов и пропусков нет (исключение стобец деталей к платежу, он не интересен).
Мы изменили формат дат на datetime64[ns]. В остальном данные готовы к исследованию.
# Для исследования данных соединим таблицы: final_ab_new_users, final_ab_events и final_ab_participants по идентификатору пользователя
data = final_ab_new_users.merge(final_ab_events, how='left', on='user_id')
data = data.merge(final_ab_participants, how='left', on='user_id')
data = data[['user_id', 'first_date', 'first_dt', 'region', 'event_dt', 'event_date', 'event_name', 'device', 'details', 'group', 'ab_test']]
data.head()# мы получили итоговую таблицу данных
| user_id | first_date | first_dt | region | event_dt | event_date | event_name | device | details | group | ab_test | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | D72A72121175D8BE | 2020-12-07 | 2020-12-07 | EU | 2020-12-07 21:52:10 | 2020-12-07 | product_page | PC | 0.0 | A | recommender_system_test |
| 1 | D72A72121175D8BE | 2020-12-07 | 2020-12-07 | EU | 2020-12-07 21:52:07 | 2020-12-07 | login | PC | 0.0 | A | recommender_system_test |
| 2 | F1C668619DFE6E65 | 2020-12-07 | 2020-12-07 | N.America | 2020-12-07 16:38:09 | 2020-12-07 | product_page | Android | 0.0 | NaN | NaN |
| 3 | F1C668619DFE6E65 | 2020-12-07 | 2020-12-07 | N.America | 2020-12-08 02:02:34 | 2020-12-08 | product_page | Android | 0.0 | NaN | NaN |
| 4 | F1C668619DFE6E65 | 2020-12-07 | 2020-12-07 | N.America | 2020-12-23 14:35:41 | 2020-12-23 | product_page | Android | 0.0 | NaN | NaN |
В сводной таблице есть пользователи, которые не участвовали в тестах.
# выделяем пользователей из региона EU
data_eu = data.query('region == "EU"')
display(data_eu.isna().sum())# у нас появились пропуски
data_eu.duplicated().sum()
# созаем таблицу ользователей из Европы, зарегистрировавшихся с 7 по 21 декабря 2020 года
data_eu_new = data_eu.query('"2020-12-22" > first_dt > "2020-12-06"')
# считаем сколько всего уникальных пользователей в этой таблице
total_users_eu_new = data_eu_new['user_id'].nunique()
print('количество уникальных новых пользователей в Европе всего: ', total_users_eu_new)
# создаем таблицу пользователей Европы зарегистрировавшихся в это время на тест recommender_system_test (rst)
data_eu_new_rst = data_eu_new.query('ab_test == "recommender_system_test"')
# считаем сколько уникальных пользователей в этой таблице (rst)
total_users_eu_new_rst = data_eu_new_rst['user_id'].nunique()
print('количество уникальных новых пользователей в Европе в тесте recommender_system_test: ', total_users_eu_new_rst)
# доля пользователей в европе с 7 по 21 декабря 2021 года rst к общему числу
share_new_eu_rst = 100*total_users_eu_new_rst/total_users_eu_new
print('доля уникальных новых пользователей в Европе в тесте recommender_system_test от всего количества в даты регистрации: ', round(share_new_eu_rst, 2), ' %')
data_eu_new.head()
user_id 0 first_date 0 first_dt 0 region 0 event_dt 3589 event_date 3589 event_name 3589 device 0 details 3589 group 227644 ab_test 227644 dtype: int64
количество уникальных новых пользователей в Европе всего: 42340 количество уникальных новых пользователей в Европе в тесте recommender_system_test: 6351 доля уникальных новых пользователей в Европе в тесте recommender_system_test от всего количества в даты регистрации: 15.0 %
| user_id | first_date | first_dt | region | event_dt | event_date | event_name | device | details | group | ab_test | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | D72A72121175D8BE | 2020-12-07 | 2020-12-07 | EU | 2020-12-07 21:52:10 | 2020-12-07 | product_page | PC | 0.0 | A | recommender_system_test |
| 1 | D72A72121175D8BE | 2020-12-07 | 2020-12-07 | EU | 2020-12-07 21:52:07 | 2020-12-07 | login | PC | 0.0 | A | recommender_system_test |
| 8 | 2E1BF1D4C37EA01F | 2020-12-07 | 2020-12-07 | EU | 2020-12-07 09:05:47 | 2020-12-07 | product_cart | PC | 0.0 | A | interface_eu_test |
| 9 | 2E1BF1D4C37EA01F | 2020-12-07 | 2020-12-07 | EU | 2020-12-10 04:13:53 | 2020-12-10 | product_cart | PC | 0.0 | A | interface_eu_test |
| 10 | 2E1BF1D4C37EA01F | 2020-12-07 | 2020-12-07 | EU | 2020-12-12 17:54:57 | 2020-12-12 | product_cart | PC | 0.0 | A | interface_eu_test |
Новых пользователей в Европе зарегистрировалост в rst тест 6351 - больше 6000. Это соответствует ТЗ. В даты регистрации на recommender_system_test (с 7 до 21 января) в него попали ровно 15% от общего числа новых пользователей из EU. Это условие 3 ТЗ выполнено тоже.
# Добавляем столбец в таблицу final_ab_data_eu_new_rst
data_eu_new_rst['14_date'] = data_eu_new_rst['first_dt']+timedelta(days=14)
# проверим, сколько пользователей попало в период больше чем 14 дней в recommender_system_test
delet = data_eu_new_rst[data_eu_new_rst['event_dt'] >= data_eu_new_rst['14_date'].dt.date]
display(delet.info())
<class 'pandas.core.frame.DataFrame'> Int64Index: 800 entries, 156 to 416868 Data columns (total 12 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 800 non-null object 1 first_date 800 non-null object 2 first_dt 800 non-null datetime64[ns] 3 region 800 non-null object 4 event_dt 800 non-null datetime64[ns] 5 event_date 800 non-null object 6 event_name 800 non-null object 7 device 800 non-null object 8 details 800 non-null float64 9 group 800 non-null object 10 ab_test 800 non-null object 11 14_date 800 non-null datetime64[ns] dtypes: datetime64[ns](3), float64(1), object(8) memory usage: 81.2+ KB
None
создав таблицу с пользователями, которые совершали действия после 14 лайфтайма в таблице есть, их событий после 14 лайфтайма 800. Удалим их в соответствием с ТЗ
# создаем таблицу в которой останутся события только 14-ти дней от момента регистрации
data_eu_rst_14 = data_eu_new_rst[data_eu_new_rst['event_dt'] < data_eu_new_rst['14_date'].dt.date]
# таблица с событиями только 14 дней от регистрации пропуски
display('пропуски', data_eu_rst_14.isna().sum())
# таблица с событиями только 14 дней от регистрации удаляем пропуски
data_eu_rst_14_drop = data_eu_rst_14.dropna()
display(data_eu_rst_14_drop['ab_test'].unique())# тут остался только rst тест
# оставляем нужные столбцы
data_eu_rst_14_drop_rename = data_eu_rst_14_drop[[
'user_id', 'first_dt', 'region', 'event_dt', 'event_date', 'event_name', 'group', 'ab_test', '14_date', 'device']]
# переименовываем таблицу в понятное название
result_rst = data_eu_rst_14_drop_rename
'пропуски'
user_id 0 first_date 0 first_dt 0 region 0 event_dt 0 event_date 0 event_name 0 device 0 details 0 group 0 ab_test 0 14_date 0 dtype: int64
array(['recommender_system_test'], dtype=object)
В итоговой таблице data_eu_new_rst есть пропуски в данных, связанных с событиями (event_dt, event_date, event_name, details) а в столбце first_dt - первая дата регистрации - данные есть. Это свидетельствует о том, что пользователь неактивный, он не совершал действий. Таких пользователей мы удалили.
Также есть пропуски в столбцах 'group' и 'ab_test' - это пользователи, которые регистрировались или совершали действия но не попали в группы теста и сами тесты.
Таким образом мы создали итоговую таблицу для теста rst - result_rst.
В ней только пользователи Европы, только зарегистрированные в период с 7 по 21 декабря 2020 года, отброшены события более 14 дней от момента регистрации и удалены пропуски (неактивные пользователи).
# Добавляем столбец в таблицу data_eu_new
data_eu_new['14_date'] = data_eu_new['first_dt']+timedelta(days=14)
display(data_eu_new.head())
display(data_eu_new.info())
# создаем таблицу в которой останутся события только 14-ти дней от момента регистрации
data_eu_14 = data_eu_new[data_eu_new['event_dt'] < data_eu_new['14_date'].dt.date]
# таблица с событиями только 14 дней от регистрации пропуски
data_eu_14.isna().sum()
# таблица с событиями только 14 дней от регистрации удаляем пропуски
data_eu_14_drop = data_eu_14.dropna()
display(data_eu_14_drop['ab_test'].unique())# тут оба теста
# оставляем нужные столбцы
data_eu_14_drop_rename = data_eu_14_drop[['user_id', 'first_dt', 'device', 'region', 'event_dt', 'event_date', 'event_name', 'group', 'ab_test', '14_date']]
# переименовываем таблицу в понятное название
result = data_eu_14_drop_rename
result.head()
| user_id | first_date | first_dt | region | event_dt | event_date | event_name | device | details | group | ab_test | 14_date | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | D72A72121175D8BE | 2020-12-07 | 2020-12-07 | EU | 2020-12-07 21:52:10 | 2020-12-07 | product_page | PC | 0.0 | A | recommender_system_test | 2020-12-21 |
| 1 | D72A72121175D8BE | 2020-12-07 | 2020-12-07 | EU | 2020-12-07 21:52:07 | 2020-12-07 | login | PC | 0.0 | A | recommender_system_test | 2020-12-21 |
| 8 | 2E1BF1D4C37EA01F | 2020-12-07 | 2020-12-07 | EU | 2020-12-07 09:05:47 | 2020-12-07 | product_cart | PC | 0.0 | A | interface_eu_test | 2020-12-21 |
| 9 | 2E1BF1D4C37EA01F | 2020-12-07 | 2020-12-07 | EU | 2020-12-10 04:13:53 | 2020-12-10 | product_cart | PC | 0.0 | A | interface_eu_test | 2020-12-21 |
| 10 | 2E1BF1D4C37EA01F | 2020-12-07 | 2020-12-07 | EU | 2020-12-12 17:54:57 | 2020-12-12 | product_cart | PC | 0.0 | A | interface_eu_test | 2020-12-21 |
<class 'pandas.core.frame.DataFrame'> Int64Index: 312136 entries, 0 to 449955 Data columns (total 12 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 312136 non-null object 1 first_date 312136 non-null object 2 first_dt 312136 non-null datetime64[ns] 3 region 312136 non-null object 4 event_dt 308547 non-null datetime64[ns] 5 event_date 308547 non-null object 6 event_name 308547 non-null object 7 device 312136 non-null object 8 details 308547 non-null float64 9 group 102817 non-null object 10 ab_test 102817 non-null object 11 14_date 312136 non-null datetime64[ns] dtypes: datetime64[ns](3), float64(1), object(8) memory usage: 31.0+ MB
None
array(['recommender_system_test', 'interface_eu_test'], dtype=object)
| user_id | first_dt | device | region | event_dt | event_date | event_name | group | ab_test | 14_date | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | D72A72121175D8BE | 2020-12-07 | PC | EU | 2020-12-07 21:52:10 | 2020-12-07 | product_page | A | recommender_system_test | 2020-12-21 |
| 1 | D72A72121175D8BE | 2020-12-07 | PC | EU | 2020-12-07 21:52:07 | 2020-12-07 | login | A | recommender_system_test | 2020-12-21 |
| 8 | 2E1BF1D4C37EA01F | 2020-12-07 | PC | EU | 2020-12-07 09:05:47 | 2020-12-07 | product_cart | A | interface_eu_test | 2020-12-21 |
| 9 | 2E1BF1D4C37EA01F | 2020-12-07 | PC | EU | 2020-12-10 04:13:53 | 2020-12-10 | product_cart | A | interface_eu_test | 2020-12-21 |
| 10 | 2E1BF1D4C37EA01F | 2020-12-07 | PC | EU | 2020-12-12 17:54:57 | 2020-12-12 | product_cart | A | interface_eu_test | 2020-12-21 |
Мы создали итоговую таблицу для двух тестов (recommender_system_test и interface_eu_test) в Европе - result.
В ней только пользователи Европы, только зарегистрированные в период с 7 по 21 декабря 2020 года, отброшены события более 14 дней от момента регистрации и удалены пропуски. Эта таблица нам понадобится для проверки пересечения тестов
# посмотрим первую и последнюю даты теста interface_eu_test
max_dt_eut = result[result['ab_test']=='interface_eu_test']['event_dt'].max()
min_dt_eut = result[result['ab_test']=='interface_eu_test']['event_dt'].min()
print('первая дата события interface_eu_test ', min_dt_eut)
print('первая дата события interface_eu_test ', max_dt_eut)
первая дата события interface_eu_test 2020-12-07 00:02:48 первая дата события interface_eu_test 2020-12-29 23:54:58
print('первая дата события в recommender_system_test ', result_rst['event_dt'].min())
print('последняя дата события в recommender_system_test ', result_rst['event_dt'].max())
первая дата события в recommender_system_test 2020-12-07 00:05:57 последняя дата события в recommender_system_test 2020-12-29 23:38:29
final_ab_events_max = final_ab_events['event_date'].max()
final_ab_events_max
final_ab_events_min = final_ab_events['event_date'].min()
print('первая дата всех событий', final_ab_events_min)
print('последняя дата всех событий', final_ab_events_max)
первая дата всех событий 2020-12-07 последняя дата всех событий 2020-12-30
Остановка теста recommender_system_test произошла до указанного в ТЗ 4 января 2021 года (на 5 дней раньше). Все события в исследуемых базах прекращаются 30 декабря 2020 года.
События в тесте recommender_system_test заканчиваются 29 декабря, interface_eu_test - аналогично. Есть полное пересечение 2-х времени проведения этих двух тестов в Европе.
Старт и окончание набора в тест recommender_system_test - соответствует ТЗ.
df = ab_project_marketing_events[((ab_project_marketing_events['start_dt'] >= '2020-12-07') &
(ab_project_marketing_events['start_dt'] <= '2021-12-30')) |
((ab_project_marketing_events['finish_dt'] >= '2020-12-07') &
(ab_project_marketing_events['finish_dt'] <= '2021-12-30'))
]
display(df)
| name | regions | start_dt | finish_dt | |
|---|---|---|---|---|
| 0 | Christmas&New Year Promo | EU, N.America | 2020-12-25 | 2021-01-03 |
| 10 | CIS New Year Gift Lottery | CIS | 2020-12-30 | 2021-01-07 |
во время проведения теста проводились два маркетинговых события. Только одно входило во временной промежуток исследования и действовало на территории Европы. Старт этого события - Christmas&New Year Promo - 25 декабря. Влияние этого события можно будет оценить в исследовательском анализе, когда мы будем строить динамику событий по дням. В любом случае каждый магазин ведет маркетинговую активность весь год, но на мой взгляд время рождественских расродаж выбрано неудачно. Хотя скорее всего Christmas&New Year Promo повлияло на обе группы теста.
# проверим есть пересечения 2- тестов interface_eu_test и recommender_system_test
#проверка -один и тот же пользователей в обеих группах
data_eu_eut = result[result['ab_test']=='interface_eu_test']
eut_list = list(data_eu_eut['user_id'].unique())
dubble_test_users_eu = result_rst[result_rst['user_id'].isin(eut_list)]['user_id'].nunique()
data_dub = result.pivot_table(
index='user_id', columns='ab_test', values='region', aggfunc='count'
)
print('количество пользователей присутствующих в обеих тестах:',
len(data_dub[(data_dub['interface_eu_test'] > 0)
& (data_dub['recommender_system_test'] > 0)])
)
количество пользователей присутствующих в обеих тестах: 887
# Количество и доля пользователей, участвующих в двух тестах в группе А
dubles_test_A = result.query('group == "A"')
dubles_test_A = dubles_test_A.groupby(['ab_test']).agg({'user_id':'nunique'}).reset_index()
dubles_test_A.columns = ['ab_test', 'count_unique']
# считаем долю пользователей по которым есть пересечение в двух тестах в группе А
dubles_test_A['share_%'] = round(887/dubles_test_A ['count_unique']*100,2)
dubles_test_A
| ab_test | count_unique | share_% | |
|---|---|---|---|
| 0 | interface_eu_test | 4977 | 17.82 |
| 1 | recommender_system_test | 2604 | 34.06 |
# Количество и доля пользователей, участвующих в двух тестах в группе В
dubles_test_B = result.query('group == "B"')
dubles_test_B = dubles_test_B.groupby(['ab_test']).agg({'user_id':'nunique'}).reset_index()
dubles_test_B.columns = ['ab_test', 'count_unique']
# считаем долю пользователей по которым есть пересечение в двух тестах в группе В
dubles_test_B['share_%'] = round(877/dubles_test_B ['count_unique']*100,2)
dubles_test_B
| ab_test | count_unique | share_% | |
|---|---|---|---|
| 0 | interface_eu_test | 4869 | 18.01 |
| 1 | recommender_system_test | 877 | 100.00 |
Доля пользователей, участвующих в обоих тестах в группе А 34%, а в группе В - 100%. Скорее всего именно это и есть причина почему есть различия в резульатах тестов у групп А и В
#final_ab_data_eu_rst
# проверим есть ли пересечения в 2-х группх теста
dubble_rst_ab = result_rst.pivot_table(index='user_id', columns='group', values='ab_test', aggfunc='count')
print(dubble_rst_ab.info())
print('Количество пользователей присутствующих в обеих группах:', len(dubble_rst_ab[(dubble_rst_ab['A'] > 0) & (dubble_rst_ab['B'] > 0)])
)
<class 'pandas.core.frame.DataFrame'> Index: 3481 entries, 001064FEAAB631A1 to FFF28D02B1EACBE1 Data columns (total 2 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 A 2604 non-null float64 1 B 877 non-null float64 dtypes: float64(2) memory usage: 81.6+ KB None Количество пользователей присутствующих в обеих группах: 0
Мы видим, что пересечений уникальных пользователей в группах А и В теста recommender_system_test - нет. Но есть пересечение с еще одним тестом interface_eu_test, причем в группе В все пользователи пересекаются, а в группе А их доля 34%.
# итоговая таблица теста recommender_system_test в Европе без пропусков
#с зарегистрированными пользователями с 7 по 21 декабря с событиями не менее чем 2 недели
result_rst
| user_id | first_dt | region | event_dt | event_date | event_name | group | ab_test | 14_date | device | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | D72A72121175D8BE | 2020-12-07 | EU | 2020-12-07 21:52:10 | 2020-12-07 | product_page | A | recommender_system_test | 2020-12-21 | PC |
| 1 | D72A72121175D8BE | 2020-12-07 | EU | 2020-12-07 21:52:07 | 2020-12-07 | login | A | recommender_system_test | 2020-12-21 | PC |
| 146 | DD4352CDCF8C3D57 | 2020-12-07 | EU | 2020-12-07 15:32:54 | 2020-12-07 | product_page | B | recommender_system_test | 2020-12-21 | Android |
| 148 | DD4352CDCF8C3D57 | 2020-12-07 | EU | 2020-12-08 08:29:31 | 2020-12-08 | product_page | B | recommender_system_test | 2020-12-21 | Android |
| 150 | DD4352CDCF8C3D57 | 2020-12-07 | EU | 2020-12-10 18:18:27 | 2020-12-10 | product_page | B | recommender_system_test | 2020-12-21 | Android |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 449812 | 0416B34D35C8C8B8 | 2020-12-20 | EU | 2020-12-21 22:28:29 | 2020-12-21 | product_page | A | recommender_system_test | 2021-01-03 | Android |
| 449813 | 0416B34D35C8C8B8 | 2020-12-20 | EU | 2020-12-24 09:12:51 | 2020-12-24 | product_page | A | recommender_system_test | 2021-01-03 | Android |
| 449814 | 0416B34D35C8C8B8 | 2020-12-20 | EU | 2020-12-20 20:58:25 | 2020-12-20 | login | A | recommender_system_test | 2021-01-03 | Android |
| 449815 | 0416B34D35C8C8B8 | 2020-12-20 | EU | 2020-12-21 22:28:29 | 2020-12-21 | login | A | recommender_system_test | 2021-01-03 | Android |
| 449816 | 0416B34D35C8C8B8 | 2020-12-20 | EU | 2020-12-24 09:12:49 | 2020-12-24 | login | A | recommender_system_test | 2021-01-03 | Android |
22620 rows × 10 columns
result_rst['region'].unique()
result_rst['ab_test'].unique()
result_rst['event_date'].min()
result_rst.isna().sum()
result_rst.info()
<class 'pandas.core.frame.DataFrame'> Int64Index: 22620 entries, 0 to 449816 Data columns (total 10 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 22620 non-null object 1 first_dt 22620 non-null datetime64[ns] 2 region 22620 non-null object 3 event_dt 22620 non-null datetime64[ns] 4 event_date 22620 non-null object 5 event_name 22620 non-null object 6 group 22620 non-null object 7 ab_test 22620 non-null object 8 14_date 22620 non-null datetime64[ns] 9 device 22620 non-null object dtypes: datetime64[ns](3), object(7) memory usage: 1.9+ MB
#отбираем пользователей только нашего теста из всех тестируемых - фильтруем по наименованию теста
users_test_rst = data[data['ab_test'] == 'recommender_system_test']#.groupby('user_id')
count_eu_users = users_test_rst['user_id'].nunique()
print('количество уникальных пользователей в Европе зарегистрировавшихся в recommender_system_test')
print('всего', count_eu_users)
print('после применения условий ТЗ ', total_users_eu_new_rst)
print('Тосле удаления неактивных пользователей ', result_rst['user_id'].nunique())
количество уникальных пользователей в Европе зарегистрировавшихся в recommender_system_test всего 6701 после применения условий ТЗ 6351 Тосле удаления неактивных пользователей 3481
Мы видим, что до удаления пропущенных данных для проведения recommender_system_test в Европе было зарегистрировано 6351 уникальный пользователь. Значит условие ТЗ о наборе 6000 пользователей было выполнено, однако данные содержали пропуски, поэтому реально мы будем тестировать 3481 уникальный профиль пользователя.
Также мы можем сказать что условие 15% новых пользователей в Европе для проведения нашего теста выполнено - у нас 15%. Последняя дата события в группах нашего теста 30 декабря 2020 года, значит условие что остановка теста 4 января 2021 года - выполнено.
Остается проверить последнее условие - увеличение на 10% метрик.
В нашей таблице появились пропуски в столбцах связанных с событиями. Это пользователи, которые зарегистрировались, но не совершили ни одного события, количество уникальных пассивных пользователей 2870(6351-3481, это почти 11% от общего числа данных от всех событий. В выведенном верхе таблицы - мы видим незаполненность полей связанных с событиями. Заменим названия событий на 0, и посчитаем уникальных пользователей, которые зарегистрировались для участия в тесте, но не совершили ни одного события - таковых 2870.
Мы проверили соответствие данных условию Технического Задания (ТЗ). Все условия выполнены - данные собирались в период с 7 о 21 декабря, остановка теста произошла раньше 4 января 2021 года - 30 декабря 2020 года. Перечечений пользователей в группах А и В нет. Дубликатов нет. Количество пользователей в группах разное.
Все пользователи совершали события не более 14 дней после регистрации, остальные события были отброшены.
В регионе Европа в данный промежуток времени происходило еще одно тестирование на выбранной группе пользователей - nterface_eu_test. Полное пересечение по времени, количество пользователей группы В задействовано в обоих тестах на 100%, в группе А на 34%.
Кроме того есть пересечение с маркетинговой активностью в европейском регионе - Christmas&New Year Promo - начиная с 25 декабря.
В данных есть пользователи, которые не совершили ни одного действия - 2870 пользователя.
result_rst
| user_id | first_dt | region | event_dt | event_date | event_name | group | ab_test | 14_date | device | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | D72A72121175D8BE | 2020-12-07 | EU | 2020-12-07 21:52:10 | 2020-12-07 | product_page | A | recommender_system_test | 2020-12-21 | PC |
| 1 | D72A72121175D8BE | 2020-12-07 | EU | 2020-12-07 21:52:07 | 2020-12-07 | login | A | recommender_system_test | 2020-12-21 | PC |
| 146 | DD4352CDCF8C3D57 | 2020-12-07 | EU | 2020-12-07 15:32:54 | 2020-12-07 | product_page | B | recommender_system_test | 2020-12-21 | Android |
| 148 | DD4352CDCF8C3D57 | 2020-12-07 | EU | 2020-12-08 08:29:31 | 2020-12-08 | product_page | B | recommender_system_test | 2020-12-21 | Android |
| 150 | DD4352CDCF8C3D57 | 2020-12-07 | EU | 2020-12-10 18:18:27 | 2020-12-10 | product_page | B | recommender_system_test | 2020-12-21 | Android |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 449812 | 0416B34D35C8C8B8 | 2020-12-20 | EU | 2020-12-21 22:28:29 | 2020-12-21 | product_page | A | recommender_system_test | 2021-01-03 | Android |
| 449813 | 0416B34D35C8C8B8 | 2020-12-20 | EU | 2020-12-24 09:12:51 | 2020-12-24 | product_page | A | recommender_system_test | 2021-01-03 | Android |
| 449814 | 0416B34D35C8C8B8 | 2020-12-20 | EU | 2020-12-20 20:58:25 | 2020-12-20 | login | A | recommender_system_test | 2021-01-03 | Android |
| 449815 | 0416B34D35C8C8B8 | 2020-12-20 | EU | 2020-12-21 22:28:29 | 2020-12-21 | login | A | recommender_system_test | 2021-01-03 | Android |
| 449816 | 0416B34D35C8C8B8 | 2020-12-20 | EU | 2020-12-24 09:12:49 | 2020-12-24 | login | A | recommender_system_test | 2021-01-03 | Android |
22620 rows × 10 columns
users_device = result_rst.groupby('device').agg({'user_id': 'nunique'}).sort_values(by='user_id',ascending=False)
users_device
| user_id | |
|---|---|
| device | |
| Android | 1544 |
| PC | 901 |
| iPhone | 707 |
| Mac | 329 |
rst_group = result_rst.groupby('group').agg({'user_id': 'nunique'})
plt.figure(figsize=(8, 5))
plt.pie(data=rst_group, x='user_id', labels=rst_group.index, autopct='%.1f')
plt.title('Соотношение пользователей в тесте recommender_system_test по группам')
plt.show()
Разбивка пользователей по количеству неравномерная в группе А 25,2%б в гуппе В почти 75%. Скорее всего алгоритм разделения пользователей не был настроен оптимально, вполне вероятно что не учитывалась или не могла быть учтена пользовательская активность и пользователей группы В было больше неактивных, а после удаления неактивных пользователей - получился числовой дисбалланс в пользу группы А. Поскольку группа А - контрольная, а В - тестовая, то пользователей которые тестировали улучшенную систему рекомендаций может быть просто мало для адекватной оценки результатов
#всего пользователей, без привязки по группам
users_device = result_rst.groupby('device').agg({'user_id': 'nunique'}).sort_values(by='user_id',ascending=False)
users_device = users_device.rename(columns={'user_id':'count'})
users_device['percent'] = users_device['count'] / users_device['count'].sum()*100
display(users_device)
plt.figure(figsize=(7, 4))
barplot_object = sns.barplot(x=users_device.index, y='percent', data=users_device)
barplot_object.set_title('Количество пользователей по девайсам', fontsize=18)
barplot_object.set_xlabel('девайс', fontsize=14)
barplot_object.set_ylabel('%', fontsize=14)
plt.show()
| count | percent | |
|---|---|---|
| device | ||
| Android | 1544 | 44.355070 |
| PC | 901 | 25.883367 |
| iPhone | 707 | 20.310256 |
| Mac | 329 | 9.451307 |
#всего пользователей, без привязки по группам
users_device = result_rst.groupby('device').agg({'user_id': 'nunique'}).sort_values(by='user_id',ascending=False)
users_device = users_device.rename(columns={'user_id':'count'})
users_device['percent'] = round(users_device['count'] / users_device['count'].sum()*100, 0)
display(users_device)
plt.figure(figsize=(7, 4))
barplot_object = sns.barplot(x=users_device.index, y='percent', data=users_device)
barplot_object.set_title('Количество пользователей по девайсам', fontsize=18)
barplot_object.set_xlabel('девайс', fontsize=14)
barplot_object.set_ylabel('%', fontsize=14)
plt.show()
#группа А
users_deviceA = result_rst[result_rst['group']=='A'].groupby('device').agg({'user_id': 'nunique'}).sort_values(by='user_id',ascending=False)
users_deviceA = users_deviceA.rename(columns={'user_id':'count'})
users_deviceA['percent'] = round(users_deviceA['count'] / users_deviceA['count'].sum()*100, 0)
display(users_deviceA)
plt.figure(figsize=(7, 4))
barplot_object = sns.barplot(x=users_deviceA.index, y='percent', data=users_deviceA)
barplot_object.set_title('Количество пользователей группы А по девайсам', fontsize=18)
barplot_object.set_xlabel('девайс', fontsize=14)
barplot_object.set_ylabel('%', fontsize=14)
plt.show()
#группа B
users_deviceB = result_rst[result_rst['group']=='B'].groupby('device').agg({'user_id': 'nunique'}).sort_values(by='user_id',ascending=False)
users_deviceB = users_deviceB.rename(columns={'user_id':'count'})
users_deviceB['percent'] = round(users_deviceB['count'] / users_deviceB['count'].sum()*100, 0)
display(users_deviceB)
plt.figure(figsize=(7, 4))
barplot_object = sns.barplot(x=users_deviceB.index, y='percent', data=users_deviceB)
barplot_object.set_title('Количество пользователей группы B по девайсам', fontsize=18)
barplot_object.set_xlabel('девайс', fontsize=14)
barplot_object.set_ylabel('%', fontsize=14)
plt.show()
print('Количество пользователей в группе А:', users_deviceA['count'].sum())
print('Количество пользователей в группе B:', users_deviceB['count'].sum())
| count | percent | |
|---|---|---|
| device | ||
| Android | 1544 | 44.0 |
| PC | 901 | 26.0 |
| iPhone | 707 | 20.0 |
| Mac | 329 | 9.0 |
| count | percent | |
|---|---|---|
| device | ||
| Android | 1139 | 44.0 |
| PC | 689 | 26.0 |
| iPhone | 521 | 20.0 |
| Mac | 255 | 10.0 |
| count | percent | |
|---|---|---|
| device | ||
| Android | 405 | 46.0 |
| PC | 212 | 24.0 |
| iPhone | 186 | 21.0 |
| Mac | 74 | 8.0 |
Количество пользователей в группе А: 2604 Количество пользователей в группе B: 877
users_device_1 = users_device.merge(users_deviceA, on = 'device', how='left')
users_device_2 = users_device_1.merge(users_deviceB, on = 'device', how='left')
#группа А
#группа B
users_device_2.columns = ['count', 'percent', 'count_A', 'percent_A', 'count_B', 'percent_B']
users_device_2 = users_device_2[['percent', 'percent_A', 'percent_B']]
fig = px.line(users_device_2, x=users_device_2.index, y=['percent', 'percent_A', 'percent_B'], \
title='Сводная: процент пользователей девайсов всего и с разбивкой по группам в %%')
fig.show()
users_device_2
| percent | percent_A | percent_B | |
|---|---|---|---|
| device | |||
| Android | 44.0 | 44.0 | 46.0 |
| PC | 26.0 | 26.0 | 24.0 |
| iPhone | 20.0 | 20.0 | 21.0 |
| Mac | 9.0 | 10.0 | 8.0 |
от 44 до 46% пользователей это пользователи устройств Android, от 24 до 26% - персональных компьютеров, 20-21% iPhone и от 8 до 10% компьютероа МАС. При разнице в количестве пользователей в 50% разница в разбивке по устройствам меньше статичтической погрешности. Можно считать разбивку по устройствам равномерной.
print('Данные по событиям представлены за период с ', str(result_rst['event_dt'].min()),
' по ', str(result_rst['event_dt'].max()))
print('Общее количество событий:', len(result_rst))
print('Количество событий в группе А:', len(result_rst[result_rst['group'] == 'A']))
print('Количество событий в группе B:', len(result_rst[result_rst['group'] == 'B']))
result_rst.groupby('user_id').agg({'event_dt': 'count'}).hist(bins=100)
plt.title('Распределение количества событий на пользователя без учета групповой принадлежности')
print('Количество событий на одного пользователя в группе А:',
round(len(result_rst[result_rst['group'] == 'A']) / users_deviceA['count'].sum(),0))
print('Количество событий на одного пользователя в группе B:',
round(len(result_rst[result_rst['group'] == 'B']) / users_deviceB['count'].sum(),0))
Данные по событиям представлены за период с 2020-12-07 00:05:57 по 2020-12-29 23:38:29 Общее количество событий: 22620 Количество событий в группе А: 17835 Количество событий в группе B: 4785 Количество событий на одного пользователя в группе А: 7.0 Количество событий на одного пользователя в группе B: 5.0
Построив график распределение событий по пользователям, можно сказать что оно является нормальным, поэтому расчет среднего кол-ва событий на пользователя по группам делаю по среднему. Пользователи группы А более активны, в среднем они делают 7 событий. Это больше на два события чем пользовватели группы В
# мы используем очищенные данные о событиях, поэтому пассивные пользователи не учитываются
result_rst[result_rst['group'] == 'A'].groupby('user_id').agg({'event_dt': 'count'}).hist(bins=50)
plt.title('Распределение количества событий на пользователя в группе A')
result_rst[result_rst['group'] == 'B'].groupby('user_id').agg({'event_dt': 'count'}).hist(bins=50)
plt.title('Распределение количества событий на пользователя в группе B')
print('Количество событий на одного пользователя в группе А:', round(len(result_rst[result_rst['group'] == 'A']) / users_deviceA['count'].sum(),0))
print('Количество событий на одного пользователя в группе B:', round(len(result_rst[result_rst['group'] == 'B']) / users_deviceB['count'].sum(),0))
Количество событий на одного пользователя в группе А: 7.0 Количество событий на одного пользователя в группе B: 5.0
# совместим 2 гистограммы на одном графике и уменьшим количество корзин до 15
ab = result_rst.groupby(['user_id', 'group']).agg({'event_dt': 'count'}).reset_index()
ab
#create histogram for each team
ab.hist (column='event_dt', by='group', bins= 15 , grid= True , rwidth= .9 ,
color='purple', sharex= True )
print('Распределение по группам с количеством корзин - 15')
Распределение по группам с количеством корзин - 15
Распределение событий по группам очень схоже и похоже на нормальное. Пользователей совершивших по 4 и 6 действий больше всего в выборках. в группе А пик на 6-ти событиях, в группе В - неявный пик на 4-х. При уменьшении количества корзин и совмещении гистограмм рядом - распределение более схоже. Количество событий в группе на одного пользователя в группе А - 7, В - 5
fig = px.histogram(result_rst,
x='event_date',# hue='group',
title='Всего событий по дням')
fig.show()
Распределение отдаленно напоминает нормально. 14 и 21 декабря аномалии событий. 14 декабря резкий рост после небольшого спада, а 21 декабря (день окончания регистрации на тест) - пик событий. Распределение отдаленно напоминает номальное со скосом влево. Возможно в данных есть выбросы, которые повлияли на распределение. После 21-го идет активный спад. Резкого изменения активности после 25-го - начала новогодней активности - не наблюдается. Кроме этого заметно что есть 2 "вершины" распределений на 9 декабря и спад к 14-му и на 20. Есть вероятность что это недельные циклы. Пик 21-го скорее всего связан с недобором в тест.
result_dt = result_rst.pivot_table(index=['event_date', 'group'], values='user_id',aggfunc='count')
result_dt.columns = ['count']
result_dt = result_dt.reset_index()
result_dt.info()
plt.figure(figsize=(18, 12))
sns.barplot(x='count', y=result_dt['event_date'], hue=result_dt['group'], data=result_dt)
plt.title('Распределение количества событий по дням', fontsize=18)
plt.xlabel('Количество событий')
plt.ylabel(None)
# выбираем положение легенды и указываем размер шрифта
plt.legend(loc='lower right', fontsize=15)
# добавляем сетку
plt.grid()
plt.show()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 46 entries, 0 to 45 Data columns (total 3 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 event_date 46 non-null object 1 group 46 non-null object 2 count 46 non-null int64 dtypes: int64(1), object(2) memory usage: 1.2+ KB
Если рассматривать распределение событий по группам в тесте recommender_system_test, то расределение выглядит похожим между группами. Выбросы в максимум у группы А 21 декабря, у группы В - 7, 16 и 21-го. С 22 декабря заметно большее снижение в обоих группах. До 10 декабря группа В имеет количество событий сопоставимое с группой А. Если учесть то что количество пользователей в группе И в 3 раза меньше, то получается что группа В изначально была активнее или просто соотношение пользователей по группам было другое на тот момент.
simple_product_funnel = result_rst.groupby('event_name').agg({'user_id': 'nunique'})
simple_product_funnel = simple_product_funnel.sort_values(by='user_id', ascending=False)
display(simple_product_funnel)
fig = go.Figure(
go.Funnel(
y=simple_product_funnel.index,
x=simple_product_funnel['user_id'],
)
)
fig.update_layout(title='Простая продуктовая воронка всех событий', title_x = 0.5)
fig.show()
| user_id | |
|---|---|
| event_name | |
| login | 3480 |
| product_page | 2178 |
| purchase | 1082 |
| product_cart | 1026 |
result_rst_a = result_rst[result_rst['group'] == 'A']
simple_product_funnel = result_rst_a.groupby('event_name').agg({'user_id': 'nunique'})
simple_product_funnel = simple_product_funnel.sort_values(by='user_id', ascending=False)
display(simple_product_funnel)
fig = go.Figure(
go.Funnel(
y=simple_product_funnel.index,
x=simple_product_funnel['user_id'],
)
)
fig.update_layout(title='Простая продуктовая воронка событий в группе А', title_x = 0.5)
fig.show()
| user_id | |
|---|---|
| event_name | |
| login | 2604 |
| product_page | 1685 |
| purchase | 833 |
| product_cart | 782 |
result_rst_b = result_rst[result_rst['group'] == 'B']
simple_product_funnel = result_rst_b.groupby('event_name').agg({'user_id': 'nunique'})
simple_product_funnel = simple_product_funnel.sort_values(by='user_id', ascending=False)
display(simple_product_funnel)
fig = go.Figure(
go.Funnel(
y=simple_product_funnel.index,
x=simple_product_funnel['user_id'],
)
)
fig.update_layout(title='Простая продуктовая воронка событий в группе В', title_x = 0.5)
fig.show()
| user_id | |
|---|---|
| event_name | |
| login | 876 |
| product_page | 493 |
| purchase | 249 |
| product_cart | 244 |
Посторив воронку продаж без последовательности этапов (в современных интернет магазинах реализованы сценарии оплаты как с главной страницы так и со страниц товаров, а также возможно попадание пользователя на страницу товара минуя страницу авторизации) оплата составляет около 30% от кол-ва авторизаций на сайте. Причем в группе А конверсия в покупку почти на 4% больше. Попробую рассмотреть воронку событий с учетом последовательного совершения событий, ведь recommender_system_test - тестирует улучшенную рекомендательную систему и новую воронку продаж.
product_funnel = result_rst.pivot_table(
index='user_id', columns='event_name', values='event_dt', aggfunc='min'
)
step_1 = ~product_funnel['login'].isna()
step_2 = step_1 & (product_funnel['product_page'] > product_funnel['login'])
step_3 = step_2 & (product_funnel['product_cart'] > product_funnel['product_page'])
step_4 = step_3 & (product_funnel['purchase'] > product_funnel['product_cart'])
n_login = product_funnel[step_1].shape[0]
n_product_page = product_funnel[step_2].shape[0]
n_product_cart = product_funnel[step_3].shape[0]
n_purchase = product_funnel[step_4].shape[0]
print('\033[1mПродуктовая воронка с учётом последовательности событий:\033[0m')
print('Авторизовались:', n_login)
print('Просмотрели карточку товара:', n_product_page)
print('Открыли корзину:', n_product_cart)
print('Оплатили:', n_purchase)
fig = go.Figure(
go.Funnel(
y=[
'Авторизовались',
'Просмотрели карточку товара',
'Открыли корзину',
'Оплатили',
],
x=[n_login, n_product_page, n_product_cart, n_purchase],
)
)
fig.update_layout(title='Продуктовая воронка с учётом последовательности событий', title_x = 0.5)
fig.show()
Продуктовая воронка с учётом последовательности событий:
Авторизовались: 3480
Просмотрели карточку товара: 1223
Открыли корзину: 72
Оплатили: 1
result_rst_a = result_rst[result_rst['group'] == 'A']
product_funnel = result_rst_a.pivot_table(
index='user_id', columns='event_name', values='event_dt', aggfunc='min'
)
step_1 = ~product_funnel['login'].isna()
step_2 = step_1 & (product_funnel['product_page'] > product_funnel['login'])
step_3 = step_2 & (product_funnel['product_cart'] > product_funnel['product_page'])
step_4 = step_3 & (product_funnel['purchase'] > product_funnel['product_cart'])
n_login = product_funnel[step_1].shape[0]
n_product_page = product_funnel[step_2].shape[0]
n_product_cart = product_funnel[step_3].shape[0]
n_purchase = product_funnel[step_4].shape[0]
print('\033[1mПродуктовая воронка с учётом последовательности событий:\033[0m')
print('Авторизовались:', n_login)
print('Просмотрели карточку товара:', n_product_page)
print('Открыли корзину:', n_product_cart)
print('Оплатили:', n_purchase)
fig = go.Figure(
go.Funnel(
y=[
'Авторизовались',
'Просмотрели карточку товара',
'Открыли корзину',
'Оплатили',
],
x=[n_login, n_product_page, n_product_cart, n_purchase],
)
)
fig.update_layout(title='Продуктовая воронка с учётом последовательности событий', title_x = 0.5)
fig.show()
Продуктовая воронка с учётом последовательности событий:
Авторизовались: 2604
Просмотрели карточку товара: 959
Открыли корзину: 49
Оплатили: 0
result_rst_b = result_rst[result_rst['group'] == 'B']
product_funnel = result_rst_b.pivot_table(
index='user_id', columns='event_name', values='event_dt', aggfunc='min'
)
step_1 = ~product_funnel['login'].isna()
step_2 = step_1 & (product_funnel['product_page'] > product_funnel['login'])
step_3 = step_2 & (product_funnel['product_cart'] > product_funnel['product_page'])
step_4 = step_3 & (product_funnel['purchase'] > product_funnel['product_cart'])
n_login = product_funnel[step_1].shape[0]
n_product_page = product_funnel[step_2].shape[0]
n_product_cart = product_funnel[step_3].shape[0]
n_purchase = product_funnel[step_4].shape[0]
print('\033[1mПродуктовая воронка с учётом последовательности событий:\033[0m')
print('Авторизовались:', n_login)
print('Просмотрели карточку товара:', n_product_page)
print('Открыли корзину:', n_product_cart)
print('Оплатили:', n_purchase)
fig = go.Figure(
go.Funnel(
y=[
'Авторизовались',
'Просмотрели карточку товара',
'Открыли корзину',
'Оплатили',
],
x=[n_login, n_product_page, n_product_cart, n_purchase],
)
)
fig.update_layout(title='Продуктовая воронка с учётом последовательности событий', title_x = 0.5)
fig.show()
Продуктовая воронка с учётом последовательности событий:
Авторизовались: 876
Просмотрели карточку товара: 264
Открыли корзину: 23
Оплатили: 1
При рассмотрении воронки продаж с традиционной последовательностью этапов - группа В показала лучший результат конверсии в продажу. Однако, только 1 раз была произведена оплата (4,3% от кол-ва просмотра корзины), конверсия достаточно низкая. С другой стороны воронка без учета последовательности событий показывает достаточно высокие результаты, но в группе В конверсия в продажу ниже на те же 4%.
во время проведения теста проводились два маркетинговых события. Только одно входило во временной промежуток исследования и действовало на территории Европы. Старт этого события - Christmas&New Year Promo - 25 декабря, резких скачков на графике распределения событий по дням в это время не было. Можно сделать вывод что событие не повлияло на результаты нашего теста.
Если рассматривать распределение пользователей в группах по устройствам входа, то оно схоже, разница в пределах статистической пограшности. В группе В айфонов больше чем в группе А, а персональных компьютеров меньше.
Поскольку время проведения второго теста в Европе полностью совпадает с recommender_system_test - оценить его ввлияние на графике не представляется возможным.
31% пользователей доходит до события оплаты товаров purchase. Доля пользователей просматривающих корзину покупок меньше, возможно так настроен алгоритм сайта и купить товары можно без просмотра корзины. В группе А конверсия в продажу больше чем в группе В.
В воронке продаж с учетом очередности этапов - конверсия в продажи в группе В выше чем в группа А, однако только один пользователь дошел до оплаты в этой группе, то есть мы получили ничтожный результат за 2 недели теста.
Скорее всего пользователи не соблюдают традиционную воронке продаж, потому что с учетом очередности конверсия падает до уровня менее 1%.
Н0 - доли конверсии между этапами в группах равны
Н1 - доли конверсии между этапами в группах не равны
мы сравниваем конверсии трех событий - в просмотр карточки товара, в просмотр корзины и в покупку. Это множественное сравнение. Поэтому применим поправку Бонферрони, равную 3
def stat_test(successes, trials, alpha, n_tests, event_name):
# применим поправку Бонферрони
alpha = alpha/n_tests # критический уровень статистической значимости
# пропорция успехов в первой группе:
p1 = successes[0]/trials[0]
# пропорция успехов во второй группе:
p2 = successes[1]/trials[1]
# пропорция успехов в комбинированном датасете:
p_combined = (successes[0] + successes[1]) / (trials[0] + trials[1])
# разница пропорций в датасетах
difference = p1 - p2
# считаем статистику в ст.отклонениях стандартного нормального распределения
z_value = difference / mth.sqrt(p_combined * (1 - p_combined) * (1/trials[0] + 1/trials[1]))
# задаем стандартное нормальное распределение (среднее 0, ст.отклонение 1)
distr = st.norm(0, 1)
p_value = (1 - distr.cdf(abs(z_value))) * 2
print('\033[1m' + 'Результаты теста для события', event_name + ':\033[0m')
print('Общее кол-во пользователей в группе А:', trials[0])
print('Кол-во пользователей в группе А совершивших действие',event_name, ':', successes[0])
print('Конверсия в группе А:{:.2%}'.format(successes[0]/trials[0]))
print('Общее кол-во пользователей в группе B:', trials[1])
print('Кол-во пользователей в группе B совершивших действие',event_name, ':', successes[1])
print('Конверсия в группе B:{:.2%}'.format(successes[1]/trials[1]))
print('p-alpha: ', alpha)
print('p-значение: ', p_value)
if p_value < alpha:
print('Для события', event_name, ': отвергаем нулевую гипотезу, между долями есть значимая разница')
else:
print(
'Для события', event_name, 'не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными'
)
print()
return
list_event = ['product_page', 'product_cart', 'purchase']
for elem in list_event:
stat_test( ([result_rst[(result_rst['group']=='A') &
(result_rst['event_name']==elem)]['user_id'].nunique() ,
result_rst[(result_rst['group']=='B') &
(result_rst['event_name']==elem)]['user_id'].nunique()]
),
[result_rst[result_rst['group']=='A']['user_id'].nunique(),
result_rst[result_rst['group']=='B']['user_id'].nunique()],
0.05,
3, # применим поправку Бонферрони
elem
)
Результаты теста для события product_page: Общее кол-во пользователей в группе А: 2604 Кол-во пользователей в группе А совершивших действие product_page : 1685 Конверсия в группе А:64.71% Общее кол-во пользователей в группе B: 877 Кол-во пользователей в группе B совершивших действие product_page : 493 Конверсия в группе B:56.21% p-alpha: 0.016666666666666666 p-значение: 6.942739359416805e-06 Для события product_page : отвергаем нулевую гипотезу, между долями есть значимая разница Результаты теста для события product_cart: Общее кол-во пользователей в группе А: 2604 Кол-во пользователей в группе А совершивших действие product_cart : 782 Конверсия в группе А:30.03% Общее кол-во пользователей в группе B: 877 Кол-во пользователей в группе B совершивших действие product_cart : 244 Конверсия в группе B:27.82% p-alpha: 0.016666666666666666 p-значение: 0.21469192029582396 Для события product_cart не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Результаты теста для события purchase: Общее кол-во пользователей в группе А: 2604 Кол-во пользователей в группе А совершивших действие purchase : 833 Конверсия в группе А:31.99% Общее кол-во пользователей в группе B: 877 Кол-во пользователей в группе B совершивших действие purchase : 249 Конверсия в группе B:28.39% p-alpha: 0.016666666666666666 p-значение: 0.04652482738393027 Для события purchase не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
Ожидаемого увеличения конверсии на 10% в тестовой группе не наблюдается ни в одном из событий. Наоборот мы видим снижение конверсии на 8,5, 2,21 и 3,6% соответственно (действия product_page, product_cart и purchase) Поскольку мы трижды сравниваем события в одной группе тестов, то мы применили поправку на множественность тестов и рзделили alpha = .05 - критический уровень статистической значимости на 3 сравнения. олучилось отвергнуть нулевую гипотезу при сравнении конверсии действия product_page - доли конверсий разные. Остальные действия - отвергнуть нулевую гипотезу не получилось, нет оснований считать доли разными.
При проверке соответствия проведения теста recommender_system_test условиям Технического задания мы пришли к выводу о выполнении всех пунктов технического задания кроме последнего. Количество пользователей задействованных в тесте 6351, это более необходимых 6000 человек. Количество пользователей, попавших в группы этого теста составляет 15% от общего количества пользователей, зарегистрированных а Европе. Старт и окончание теста соответствует ТЗ: с 7 по 21 декабря 2020 года. Остановка теста произошла раньше на 5 дней. В таблице для сравнения групп пользователей мы оставили события не позднее 14 дней после даты регистрации пользователя. Из зарегистрированных пользователей 2870 оказались неактивны и для чистоты эксперимента нам пришлось их удалить из данных.
Остальные пользователи зарегистрированные и учствующие в тесте разделились между группами А и В в соотношении 3:1. Распределение польователей по количеству событий похоже на нормальное со скосом влево. Больше всего (пики) на значениях 4 и 6. Среднее значение в группе А - 7 событий на человека, в группе В - 5.
Расределение по устройствам входа схожее, но в группе В незначительно больше айфонов и меньше персональных компьютеров.
Динамика распределения количества событий в группах А и В - имеет похожее распределение, и недельную цикличность. В обоих группах есть пик на 21 декабря - дата окончания регистрации на тест. Скорее всего в этот день был "добор" пользователей в ущерб качеству. Группа В в первую неделю совершала одинаковое количество событий с пользователями группы А. Во вторую неделю пользователи группы А действовали более активно чем в первую неделю. После 21 декабря количество событий в обоих группах пошло на убыль. Заметного скачка снижения активности 25 декабря из-за рождественской маркетинговой активности магазина в событии Christmas&New Year Promo начавшемся 25 декабря не наблюдаем.
Пересечения пользователей в нашем тесте по группам А и В нет. Есть пересечение с конкурирующим тестом interface_eu_test.
Ключевая метрика нашего теста это конверсия в продажи. Из условия задания А- контрольная группа, В - тестовая. В тестовой группе конверсия меньше на всех этапах. условия увеличения коверсии на 10% достигнуть не удалось. Проведение статистического анализа на равество долей конверсии между двумя группами теста показало разницу долей только на первом этапе воронки продаж product_page. Доли конверсии просмотра корзины и покупки - не получилось отвергнуть нулевую гиротезу о равенстве долей.
Тест можно признать состоявшимся, выдержавшим все условия ТЗ, но не достигнувший одидаемого результат (увеличение конверсии на 10%). В связи с этим считаю, что не стоит внедрять изменения рекомендательной системы проводимые в рамках данного теста - потому что они будут сопряжены с затратами, а ожидаемого результата не принесут. В целом считаю, что выбраны не не лучшие даты проведения тестирования, группы были разделены равномерно, но не поровну, среди пользователей большое число неактивных, которые не совершили ни одного действия.
Вывод: тест надо остановить, результаты признать неудовлетворительными.
Рекомендации: выбирать время тестирования в периоды не пересекающимися с другими маркетинговыми активностями и тестами. Отслеживать пассивность пользователей в группах тестов, Оптимизоровать алгоритм распределения пользователей по группам